package com.smallx.xproc.compiler;

import com.smallx.xpath.SimpleStaticContext;
import com.smallx.xpath.StaticContext;
import com.smallx.xpath.XPathCompiler;
import com.smallx.xpath.XPathMatcherFactory;
import com.smallx.xpath.streaming.SubsetXPathCompiler;
import com.smallx.xproc.*;
import com.smallx.xproc.compiler.graph.Edge;
import com.smallx.xproc.compiler.graph.GraphML;
import com.smallx.xproc.compiler.graph.Lattice;
import com.smallx.xproc.compiler.graph.Vertex;
import com.smallx.xproc.library.ComputePortComponentDefinition;
import com.smallx.xproc.library.Flow;
import com.smallx.xproc.library.FlowGraphComponentDefinition;
import com.smallx.xproc.library.ViewportEndComponentDefinition;
import com.smallx.xproc.library.ViewportStartComponentDefinition;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.*;
import java.net.*;
import java.util.logging.Logger;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.infoset.component.ComponentDefinition;
import org.infoset.component.ComponentRegistry;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.util.WriterItemDestination;
import org.infoset.xproc.Pipeline;
import org.infoset.xproc.PipelineCompiler;


public class FlowCompiler implements PipelineCompiler {
 
   static Logger LOG = Logger.getLogger(StreamingCompiler.class.getName());
   static public final Set<URI> NAMESPACE_EXCLUSIONS = new HashSet<URI>();

   class FlowPipeline implements Pipeline {

      Map<String,Object> properties;
      Name name;
      Set<URI> excludedNamespaces;
      Flow flow;
      ComponentDefinition def;
      
      FlowPipeline(Name name,ComponentDefinition def) {
         this.name = name;
         this.def = def;
         this.excludedNamespaces = new HashSet<URI>();
         this.properties = new TreeMap<String,Object>();
         this.properties.put("name",name);
         this.properties.put("loaded-at",new Date());
         excludedNamespaces.addAll(NAMESPACE_EXCLUSIONS);
      }
      
      public Map<String,Object> getProperties() {
         return properties;
      }
      
      public Name getName() {
         return name;
      }
      
      public Set<URI> getExcludedNamespaces() {
         return excludedNamespaces;
      }
      
      public ComponentDefinition getDefinition() {
         return def;
      }
            
   }
   
   ComponentRegistry registry;
   protected DocumentLoader loader;
   boolean debugLattice = false;
   LatticeListener latticeListener = new LatticeListener() {
      public void notifyLattice(Lattice l) {
         try {
            GraphML graphml = new GraphML();
            Writer w = new OutputStreamWriter(System.err);
            graphml.generate(l,new WriterItemDestination(w,"UTF-8"));
            w.flush();
         } catch (Exception ex) {
            ex.printStackTrace();
         }
      }
   };
   
   public FlowCompiler() 
      throws XMLException
   {
      this(com.smallx.xproc.library.Library.getRegistry());
   }
   
   public FlowCompiler(ComponentRegistry registry) {
      this.registry = registry;
      this.loader = new SAXDocumentLoader();
   }
   
   public ComponentRegistry getComponentRegistry() {
      return registry;
   }
   
   public void setComponentRegistry(ComponentRegistry newRegistry) {
      registry = newRegistry;
   }

   public DocumentLoader getDocumentLoader() {
      return loader;
   }

   public void setDocumentLoader(DocumentLoader loader) {
      this.loader = loader;
   }
   
   public Pipeline compile(URI location)
      throws XMLException
   {
      try {
         Document doc = loader.load(location);
         return compile(doc.getDocumentElement());
      } catch (IOException ex) {
         throw new XMLException("Cannot load pipeline "+location+" due to I/O exception.",ex);
      }
   }
   
   public Pipeline compile(Document spec) 
      throws XMLException
   {
      return compile(spec.getDocumentElement());
   }
   
   public Pipeline compile(Element spec) 
      throws XMLException
   {
      
      if (registry==null) {
         throw new XMLException("The component registry must be set before pipelines can be compiled.");
      }

      PipelineModel model = null;
      try {
         PipelineModelBuilder builder = new PipelineModelBuilder();
         model = builder.build(spec);
         model.resolve();
      } catch (ModelErrorException ex) {
         throw new XMLException(ex.getMessage(),ex);
      }
      
      ComponentDefinition def = assignComponentAssembly(model,registry);
      
      Pipeline pipeline = new FlowPipeline(model.getName(),def);
      //pipeline.getProperties().put("location",spec.getBaseURI());
      return pipeline;
      
   }
   
   protected ComponentDefinition assignComponentAssembly(PipelineModel model,ComponentRegistry registry)
      throws XMLException
   {
      
      XPathCompiler xpathCompiler = new SubsetXPathCompiler(null);
      
      FlowGraph flowGraph = new FlowGraph(model);
      flowGraph.compile();
      flowGraph.analyze();
      
      ComponentDefinition identityDef = registry.get(Name.create(PipelineModelBuilder.NAMESPACE,"identity"));
      ComponentDefinition joinDef = registry.get(FlowGraph.JOIN_TYPE);
      ComponentDefinition gateDef = registry.get(FlowGraph.GATE_TYPE);
      
      Lattice inputLattice = flowGraph.getLattice();
      inputLattice.index();
      Set<Vertex> inputs = new HashSet<Vertex>();
      Set<Vertex> outputs = new HashSet<Vertex>();
      FlowGraphComponentDefinition flowDef = new FlowGraphComponentDefinition(model.getName());
      List<Vertex> viewports = new ArrayList<Vertex>();
      for (Vertex v : inputLattice.getVertices()) {
         Name type = v.getType();
         if (type.equals(FlowGraph.IN_TYPE)) {
            inputs.add(v);
         } else if (type.equals(FlowGraph.OUT_TYPE)) {
            outputs.add(v);
         } else if (type.equals(FlowGraph.JOIN_TYPE)) {
            flowDef.addComponent(v.getName(),joinDef,null);
         } else if (type.equals(FlowGraph.GATE_TYPE)) {
            flowDef.addComponent(v.getName(),gateDef,null);
         } else if (type.equals(FlowGraph.SOURCE_TYPE)) {
            // Do nothing
         } else if (type.equals(FlowGraph.VIEWPORT_START_TYPE)) {
            viewports.add(v);
            ComponentType startType = new ComponentType(v.getName());
            Name portName = inputLattice.getInbound(v).iterator().next().getInputPort();
            //LOG.info("Input to viewport: "+portName);
            startType.addInputPort(portName,false);
            startType.addOutputPort(ViewportStartComponentDefinition.NON_MATCHES_PORT,false);
            startType.addOutputPort(portName,false);
            ViewportStartComponentDefinition viewStartDef = new ViewportStartComponentDefinition(startType);
            /* Replaced by vertex expression
            for (Edge e : v) {
               if (e.getTarget().getType().equals(FlowGraph.VIEWPORT_END_TYPE)) {
                  continue;
               }
               if (e.getTarget().getType().equals(FlowGraph.GATE_TYPE)) {
                  continue;
               }
               String xpathExpr = e.getExpression();
               if (xpathExpr==null) {
                  throw new XMLException("No XPath expression for viewport port on edge "+e.getLabel());
               }
               StaticContext staticContext = new MapStaticContext(e.getNamespaceScope().getDataProxy());
               StreamingXPathExpression xpath = (StreamingXPathExpression)xpathCompiler.compile(staticContext,xpathExpr);
               viewStartDef.setExpression(xpath);
            }
             */
            String xpathExpr = v.getExpression();
            if (xpathExpr==null) {
               throw new XMLException("No XPath expression for viewport start "+v.getLabel());
            }
            StaticContext staticContext = new SimpleStaticContext(null,v.getExpressionNamespaceScope().getDataProxy(),SimpleStaticContext.emptyVariables);
            XPathMatcherFactory xpath = xpathCompiler.compileMatcher(staticContext,xpathExpr);
            viewStartDef.setExpression(xpath);
            flowDef.addComponent(v.getName(),viewStartDef,null);
         } else if (type.equals(FlowGraph.VIEWPORT_END_TYPE)) {
            ComponentType endType = new ComponentType(v.getName());
            Name portName = v.iterator().next().getOutputPort();
            endType.addInputPort(portName,true);
            endType.addInputPort(ViewportStartComponentDefinition.NON_MATCHES_PORT,false);
            endType.addOutputPort(portName,false);
            ViewportEndComponentDefinition viewEndDef = new ViewportEndComponentDefinition(endType);
            flowDef.addComponent(v.getName(),viewEndDef,null);
         } else if (type.equals(FlowGraph.COMPUTE_TYPE)) {
            ComputePortComponentDefinition def = new ComputePortComponentDefinition();
            String xpathExpr = v.getExpression();
            if (xpathExpr==null) {
               throw new XMLException("No XPath expression for viewport start "+v.getLabel());
            }
            StaticContext staticContext = new SimpleStaticContext(null,v.getExpressionNamespaceScope().getDataProxy(),SimpleStaticContext.emptyVariables);
            XPathMatcherFactory xpath = xpathCompiler.compileMatcher(staticContext,xpathExpr);
            def.setExpression(xpath);
            flowDef.addComponent(v.getName(),def,v.getNamespaceScope());            
         } else {
            //LOG.info("Assinging "+v.getName()+" type "+type);
            ComponentDefinition def = registry.get(type);
            if (def==null) {
               throw new XMLException("No component definition was found for component type "+type);
            }
            if (v.getExpression()!=null) {
               throw new XMLException("Unable to handle expression on vertex type "+type);
            }
            flowDef.addComponent(v.getName(),def,v.getNamespaceScope());
            if (v.hasParameters()) {
               flowDef.bindParameters(v.getName(),v.getParameters());
            }
         }
      }
      
      // Setup pipeline inputs
      int sinkNumber = 0;
      for (Vertex input : inputs) {
         Name portName = input.getName();
         //LOG.info("Adding input port "+portName);
         if (input.size()>1) {
            throw new XMLException("Join for pipeline input "+portName+" not refined ("+input.size()+" inputs remain).");
         }
         if (input.size()==0) {
            sinkNumber++;
            Name sinkName = Name.create(FlowGraph.NAMESPACE,"sink"+sinkNumber);
            flowDef.addComponent(sinkName,identityDef,null);
            flowDef.addInputPort(portName,sinkName,Name.create("document"));
         } else {
            Edge e = input.iterator().next();
            flowDef.addInputPort(portName,e.getTarget().getName(),e.getInputPort());
         }
      }
      // Setup pipeline outputs
      for (Vertex output : outputs) {
         Name portName = output.getName();
         //LOG.info("Adding output port "+portName);
         Set<Edge> sources = inputLattice.getInbound(output);
         if (sources.size()>1) {
            throw new XMLException("Meet for pipeline output "+portName+" should be impossible.");
         }
         if (sources.size()==0) {
            throw new XMLException("No source for pipeline output "+portName+" should be impossible.");
         } else {
            Edge e = sources.iterator().next();
            flowDef.addOutputPort(portName,e.getSource().getName(),e.getOutputPort());
         }
      }
      // Add the flow connects
      for (Edge e : inputLattice) {
         Name sourceType = e.getSource().getType();
         Name targetType = e.getTarget().getType();
         if (e.getExpression()!=null) {
            throw new XMLException("Edge expressions for computed inputs are not supported (expression on edge "+e.getLabel()+")");
         }
         if (sourceType.equals(FlowGraph.VIEWPORT_START_TYPE) &&
             targetType.equals(FlowGraph.GATE_TYPE)) {
            // It is a notification edge for the viewport iteration
            flowDef.addNotification(e.getSource().getName(),e.getTarget().getName());
         } else if (sourceType.equals(FlowGraph.SOURCE_TYPE)) {
            // It is a static binding for an input
            Vertex v = e.getSource();
            if (v instanceof LocationVertex) {
               flowDef.bindInput(e.getTarget().getName(),e.getInputPort(),((LocationVertex)v).getLocation());
            } else if (v instanceof DocumentSourceVertex) {
               flowDef.bindInput(e.getTarget().getName(),e.getInputPort(),((DocumentSourceVertex)v).getDocument());
            }
         } else if (targetType.equals(FlowGraph.OUT_TYPE) ||
                    sourceType.equals(FlowGraph.IN_TYPE)) {
            // Do nothing
         } else {
            flowDef.bindOutput(e.getSource().getName(),e.getOutputPort(),e.getTarget().getName(),e.getInputPort());
         }
      }
      
      // Handle iteration notifications
      for (Vertex viewportStart : viewports) {
         Set<Vertex> subgraph = flowGraph.viewportSubgraph(viewportStart);
         Vertex end = null;
         for (Edge e : viewportStart) {
            if (e.getTarget().getType().equals(FlowGraph.VIEWPORT_END_TYPE)) {
               end = e.getTarget();
            }
         }
         for (Vertex member : subgraph) {
            if (member==viewportStart || member==end) {
               continue;
            }
            flowDef.setComponentNested(member.getName());
            flowDef.addNotification(viewportStart.getName(),member.getName());
         }
      }
      
      return flowDef;
   }
      
   public DocumentLoader getLoader() {
      return loader;
   }

   public void setFeatureProperty(String id,Object value) {
      if (id.equals("http://www.smallx.com/XProc/Features/Lattice/Listener")) {
         latticeListener = (LatticeListener)value;
         return;
      }
      throw new IllegalArgumentException("Feature "+id+" is not supported.");
   }

   public void setFeature(String id,boolean flag) {
      if (id.equals("http://www.smallx.com/XProc/Features/Lattice/Debug")) {
         debugLattice = flag;
         return;
      }
      throw new IllegalArgumentException("Feature "+id+" is not supported.");
   }


}
